using System;
using System.Collections.Generic;
using System.Text;
using System.Timers;

using OWED.Network.Messages;
using OWED.Network.Messages.General;
using OWED.Network.Messages.Reply;
using OWED.Network;
using OWED.Document;

using System.Data;
using System.Data.Odbc;

using System.Xml;

namespace OWED.Backend
{
    public class UserInfo
    {
        public User user;
        public Password pass;
        public int rights;
        public bool LoggedIn;
        public bool isLoggedIn (String username) {
            if (user.name == username)
                return LoggedIn;
            else
                return false;
        }
    }

    /// <summary>
    /// Session is called from inside a thread that represents one connection to a Client.
    /// </summary>
    class Session
    {
        private Object me;  // I don't know what this is for, I think it's just an anchor to the parent thread.
        private MessageHandler.waitAsParam Wait; // function pointers to caller's parent's method
        private MessageHandler.replyAsParam Reply; // this means the caller must necessarily be a child of a server.
        private Timer sessionTime; // 
        private DBConn sql;
        private UserInfo user;
        public bool IsLoggedIn { get { return user.LoggedIn; } }

        public Session(Object o, MessageHandler.waitAsParam DelWait, MessageHandler.replyAsParam DelReply)
        {
            me = o;
            Wait = DelWait;
            Reply = DelReply;
            user = new UserInfo();
            sql = new DBConn();
        }

        /// <summary>
        /// This event is later attached to the timer. 
        /// When the timer expires, it means that the 
        /// user has been inactive for too long, so we 
        /// log out the user and close the connection.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="e"></param>
        private void TimeOutEvent(object source, ElapsedEventArgs e)
        {
            user.LoggedIn = false;
            sql.Close();
            return;
        }

        /// <summary>
        /// This must always be the first message passed from the client.
        /// It validates the username and password against the database, then
        /// initializes the private UserInfo class if valid. This is also where
        /// permissions would be retrieved and stored once implimented.
        /// </summary>
        /// <returns>true if logged in, else false.</returns>
        public bool Login()
        {
            IProtocolItem msg = Wait(me);
            if ((msg as Login) == null)
            {
                Console.WriteLine("Login type failure:");
                Console.WriteLine(msg.GetType().Name);
                Reply(me, new PassFail(false, new Reason(1, "You have to login first!")));
                return false;
            }
            else
            {
                try
                {
                    sql.Open();
                }
                catch (Exception)
                {
                    Reply(me, new PassFail(false, new Reason(-1, "Could not connect to DB")));
                    return false;
                }

                bool success = false;
                try
                {
                    success = sql.Login((msg as Login).User.name, (msg as Login).Password.value);
                    if (success)
                    {
                        Reply(me, new PassFail(true, new Reason(0, "Sucessful login!")));
                        user.LoggedIn = true;
                        user.user = (msg as Login).User;
                        user.pass = (msg as Login).Password;
                    }
                    else
                    {
                        Console.WriteLine("Tried and failed to log in: " + (msg as Login).User.name + " " + (msg as Login).Password.value);
                        Reply(me, new PassFail(false, new Reason(-1, "Invalid Username and Password ")));
                    }
                    return success;
                }
                catch (Exception e)
                {
                    Reply(me, new PassFail(success, new Reason(-1, e.Message)));
                    return false;
                }
            }
        }

        
        /// <summary>
        /// This is the big cheese. It takes one message and replies once. 
        /// Multiple messages must be handled by calling this method many times.
        /// This method initializes and sets a timer to log out the user if the 
        /// no request is made within 5 minutes.
        /// 
        /// </summary>
        public void HandleOneMessage()
        {
            // reset the timer.
            if (sessionTime != null) sessionTime.Close();
            sessionTime = new Timer(5 * 60 * 1000); // 5 minutes;
            sessionTime.Elapsed += new ElapsedEventHandler(TimeOutEvent);
            sessionTime.AutoReset = false;
            sessionTime.Start();

            // wait for message
            IProtocolItem msg = Wait(me);

            

            // If the timer timed out, user.LoggedIn will be false.
            if (!user.LoggedIn)
            {
                sessionTime.Close();
                Reply(me, new PassFail(false, new Reason(-1, "Time out due to five minutes of inactivity.")));
                return;
            }

            // This is where the message actually get handled.
            String msgType = msg.GetType().Name;
            if (msgType == "Logout")
            {
                sessionTime.Close();
                user.LoggedIn = false;
                Reply(me, new PassFail(true, new Reason(0, "Goodbye!")));
                sql.Close();
                return;
            }
            else if (msgType == "CreateUser" && (msg as CreateUser).User.name == user.user.name)
            {
                CreateUser c = (msg as CreateUser);
                bool success = sql.CreateUser(c.User.name, c.Password.value);
                if (success)
                {
                    Reply(me, new PassFail(true, new Reason(0, "created")));
                }
                else
                {
                    Reply(me, new PassFail(false, new Reason(-1, "failed")));
                }
            }
            else if (msgType == "GetDocumentList")
            {
                List<DocName> docs = new List<DocName>();
                DataSet ds = sql.getDocumentList();
                DataRowCollection drc = ds.Tables[0].Rows;
                foreach (DataRow dr in drc) {
                    docs.Add(new DocName(dr["Title"] as string));
                }
                Reply(me, new DocumentListReply(docs));
            }
            else if (msgType == "Create") 
            {
                Create c = msg as Create;
                bool success = sql.createDocument("<book></book>",c.DocName.name);
                Reply(me, new PassFail(success, new Reason(success?0:-1,success?"Created":"Create failed")));
            }
            else if (msgType == "GetDocument")
            {
                GetDocument gd = msg as GetDocument;
                DataSet ds;
                if (gd.DocVersion == null)
                {
                    ds = sql.getDocument(gd.DocName.name);
                }
                else
                {
                    ds = sql.getDocument(gd.DocName.name, gd.DocVersion.version);
                }
                XmlDocument xml = new XmlDocument();

                Byte[] ba = (Byte[])ds.Tables[0].Rows[0]["Content"];
                String content = Encoding.UTF8.GetString(ba);
                xml.LoadXml(content);
                Reply(me, new DocumentReply(new ADocument(xml, gd.DocName)));
            }
            else if (msgType == "ImportDocument")
            {
                // Takes in an entire document in serialized string form
                // and inserts it into the database;
                ImportDocument id = (msg as ImportDocument);
                bool success = sql.createDocument(id.ADocument.GetDocument().OuterXml.Replace("'","\\'"), id.DocName.name);
                Reply(me, new PassFail(success, new Reason(success ? 0 : -1, success ? "Document added." : "Could not insert document.")));
            }
            else if (msgType == "DeleteDocument")
            {
                DeleteDocument dd = msg as DeleteDocument;
                bool success = sql.deleteDocument(dd.DocName.name);
                Reply(me, new PassFail(success, new Reason(success?0:-1, success?dd.DocName.name + " deleted.":"Could not delete "+dd.DocName.name)));
            }
            else if (msgType == "GetPermissions")
            {
                GetPermissions gp = msg as GetPermissions;
                int permission = sql.getPermission(gp.foruser, gp.DocName.name);

                int READ = 1, WRITE = 2, ADMIN = 4;
                bool read = false, write = false, admin = false;
                read=((int)(permission & READ)==READ)?true:false;
                write=((int)(permission & WRITE)==WRITE)?true:false;
                admin=((int)(permission & ADMIN)==ADMIN)?true:false;
                Reply(me, new PermissionReply(new Permission(read, write, admin)));
            }
            else if (msgType == "SetPermissions")
            {
                SetPermissions sp = msg as SetPermissions;
                int permission = (sp.Permission.admin?4:0) + (sp.Permission.write?2:0) + (sp.Permission.read?1:0);
                bool success = sql.setPermission(sp.User.name, sp.DocName.name, permission);
                Reply(me, new PassFail(success, new Reason(success ? 0 : -1, success ? "Permissions changed." : "Could not change permissions.")));
            }
            else if (msgType == "ApproveProposal")
            {
                // Creates a new Version of the Document 
                // identical to the Proposal of the previous version
                ApproveProposal ap = msg as ApproveProposal;
                bool success = sql.approveProposal(ap.ProposalID.number);
                if (success)
                {
                    DataSet ds = sql.getDocument(ap.DocName.name);
                    XmlDocument xml = new XmlDocument();
                    xml.LoadXml(((string)(ds.Tables[0].Rows[0]["Content"])).Replace("'", "\\'"));
                    ADocument doc = new ADocument(xml, ap.DocName);
                    
                    ds = sql.getChangeAtoms(ap.ProposalID.number, ap.DocName.name);
                    List<ChangeAtom> changes = new List<ChangeAtom>();
                    for (int i = 0; i < ds.Tables[0].Rows.Count; i++ )
                    {
                        ChangeAtom change = ChangeAtom.FromSerialized(ds.Tables[0].Rows[i]["Content"] as string);
                        change.Apply(doc.GetDocument());
                    }
                    Reply(me, new PassFail(true, new Reason(0, "approved")));
                }
                else
                {
                    Reply(me, new PassFail(false, new Reason(0, "rejected")));
                }
            }
            else if (msgType == "ListProposals")
            {
                // Returns a list of proposals without ChangeAtoms included.
                ListProposals list_prop = msg as ListProposals;
                int version = 0;
                if (list_prop.DocVersion == null)
                {
                    DataSet ds1 = sql.ListVersions(list_prop.DocName.name);
                    for (int i = 0; i < ds1.Tables[0].Rows.Count; ++i)
                        if ((int)ds1.Tables[0].Rows[i]["StorageVersion"] > version)
                            version = (int)ds1.Tables[0].Rows[i]["StorageVersion"];
                }
                else
                {
                    version = list_prop.DocVersion.version;
                }

                DataSet ds = sql.getProposalMeta(list_prop.DocName.name,version);
                List<Proposal> prop_list = new List<Proposal>();
                for (int i = 0; i < ds.Tables[0].Rows.Count; ++i)
                {
                    bool applied = ((short)ds.Tables[0].Rows[i]["Applied"]) == 1;
                    ProposalID id = new ProposalID((int)ds.Tables[0].Rows[i]["ProposalID"]);
                    DateTime dt = (DateTime)ds.Tables[0].Rows[i]["SubmitDate"];
                    Date adate = new Date(dt.Year,dt.Month,dt.Day, 0, 0, 0);
                    dt = (DateTime)ds.Tables[0].Rows[i]["ApplyDate"];
                    Date pdate = new Date(dt.Year, dt.Month, dt.Day, 0, 0, 0);

                    prop_list.Add(new Proposal(applied, id, list_prop.DocName, 
                        new Changes(new List<Change> ()),
                        new Proposed(new Date(0, 0, 0, 0, 0, 0)),
                        new Applied(new Date(0, 0, 0, 0, 0, 0))));
                }
                Reply(me, new ProposalListReply(prop_list));
            }
            else if (msgType == "ListProposalsWithChangeAtoms")
            {
                ListProposalsWithChangeAtoms gpl = msg as ListProposalsWithChangeAtoms;
                int version = 0;
                if (gpl.DocVersion == null)
                {
                    DataSet ds1 = sql.ListVersions(gpl.DocName.name);
                    for (int i = 0; i < ds1.Tables[0].Rows.Count; ++i)
                        if ((int)ds1.Tables[0].Rows[i]["StorageVersion"] > version)
                            version = (int)ds1.Tables[0].Rows[i]["StorageVersion"];
                }
                else
                {
                    version = gpl.DocVersion.version;
                }

                DataSet ds = sql.getProposals(gpl.DocName.name, version);
                List<Proposal> prop_list = new List<Proposal>();
                for (int i = 0; i < ds.Tables[0].Rows.Count; ++i)
                {
                    bool applied = ((short)ds.Tables[0].Rows[i]["Applied"]) == 1;
                    ProposalID id = new ProposalID((int)ds.Tables[0].Rows[i]["ProposalID"]);
                    if (gpl.ProposalID != null)
                    {
                        if (id.number == gpl.ProposalID.number)
                        {
                            DateTime dt = (DateTime)ds.Tables[0].Rows[i]["SubmitDate"];
                            Date adate = new Date(dt.Year, dt.Month, dt.Day, 0, 0, 0);
                            dt = (DateTime)ds.Tables[0].Rows[i]["ApplyDate"];
                            Date pdate = new Date(dt.Year, dt.Month, dt.Day, 0, 0, 0);
                            List<Change> changes = new List<Change>();
                            for (int j = 0; j < ds.Tables[1].Rows.Count; j++)
                            {
                                changes.Add(new Change(new Text(ds.Tables[1].Rows[j]["Content"].ToString())));
                            }

                            prop_list.Add(new Proposal(applied, id, gpl.DocName,
                                new Changes(changes),
                                new Proposed(pdate),
                                new Applied(adate)));
                        }
                    }
                    else
                    {
                        DateTime dt = (DateTime)ds.Tables[0].Rows[i]["SubmitDate"];
                        Date adate = new Date(dt.Year, dt.Month, dt.Day, 0, 0, 0);
                        dt = (DateTime)ds.Tables[0].Rows[i]["ApplyDate"];
                        Date pdate = new Date(dt.Year, dt.Month, dt.Day, 0, 0, 0);
                        List<Change> changes = new List<Change>();
                        for (int j = 0; j < ds.Tables[1].Rows.Count; j++)
                        {
                            changes.Add(new Change(new Text(ds.Tables[1].Rows[j]["Content"].ToString())));
                        }

                        prop_list.Add(new Proposal(applied, id, gpl.DocName,
                            new Changes(changes),
                            new Proposed(pdate),
                            new Applied(adate)));
                    }
                }
                Reply(me, new ProposalListReply(prop_list));
            }
            else if (msgType == "ProposeVersion") // aka: SubmitProposal
            {
                ProposeVersion pv = msg as ProposeVersion;
                List<string> changes = new List<string>();
                for (int i = 0; i < pv.Changes.ChangeAtoms.Count; i++)
                {
                    changes.Add(pv.Changes.ChangeAtoms[i].Text.Value);
                }
                bool success = sql.submitProposal(pv.DocName.name, pv.User.name, changes);
                Reply(me, new PassFail(success, new Reason(success ? 0 : -1, success ? "New Proposal added" : "Could not create new proposal.")));
            }
            else if (msgType == "ListVersions")
            {
                ListVersions lv = msg as ListVersions;
                List<DocVersion> list = new List<DocVersion>();
                DataSet ds = sql.ListVersions(lv.DocName.name);
                for (int i = 0; i < ds.Tables[0].Rows.Count; i++)
                {
                    list.Add(new DocVersion(Int32.Parse(ds.Tables[0].Rows[i].ItemArray[0].ToString())));
                }
                Reply(me, new VersionReply(list));
            }
            else
            {
                Reply(me, new PassFail(false, new Reason(0, "Unknown message type: " + msg.GetType().Name)));
            }
            return;
        }
    }
}
